/**
 * TicTacToe
 * 
 * @param {Object} view The view that renders the game state
 */
function TicTacToe(view) {
	this.view = view;
}

TicTacToe.prototype.start = function(width, height) {
	this.board = new GameBoard(width, height);
	this.player = Player.X;

	this.view.init(this);
	this.updateView();
};

TicTacToe.prototype.move = function(row, col) {
	var placed = this.board.place(row, col, this.player);
	var boardState = null;
	if (placed) {
		boardState = new BoardStateEvaluator(this.board).evaluate();

		if (boardState.isInProgress()) {
			this.player = this.player.next();
		}
	}

	this.updateView();

	if (boardState != null) {
		if (boardState.hasWinner()) {
			this.view.onWin(boardState.winner(), boardState.cells());
		} else if (boardState.isInStale()) {
			this.view.onStale();
		}
	}
};

TicTacToe.prototype.updateView = function() {
	this.view.update(this.board);
};

TicTacToe.prototype.getCurrentPlayer = function() {
	return this.player;
};

TicTacToe.prototype.getBoard = function() {
	return this.board;
};

/**
 * GameBoard
 * 
 * @constructor
 * @param {number} width The width of the game board
 * @param {number} height The height of the game board
 */
function GameBoard(width, height) {
	this.width = width;
	this.height = height;
	this.NUM_TO_WIN = 5;
	this.countMoves = 0;

	this.board = [];
}

GameBoard.prototype.getWidth = function() {
	return this.width;
};

GameBoard.prototype.getHeight = function() {
	return this.height;
};

GameBoard.prototype.get = function(row, col) {
	var result = this.board[this.getLinearCoords(row, col)];

	if (result == null) {
		result = Player.NONE;
	}

	return result;
};

GameBoard.prototype.place = function(row, col, value) {
	var placed = this.canPlace(row, col);
	if (placed) {
		this.countMoves++;
		this.board[this.getLinearCoords(row, col)] = value;
	}

	return placed;
};

GameBoard.prototype.canPlace = function(row, col) {
	return this.board[this.getLinearCoords(row, col)] == null;
};

GameBoard.prototype.getLinearCoords = function(row, col) {
	return row * this.width + col;
};

/**
 * @constructor
 * @param {Object} board The game board
 */
function BoardStateEvaluator(board) {
	this.board = board;
}

BoardStateEvaluator.prototype.evaluate = function() {

	function staleResponse() {
		return {
			state : function() {
				return 'STALE';
			},
			winner : function() {
				return null;
			},
			cells : function() {
				return [];
			},
			isInProgress : function() {
				return false;
			},
			isInStale : function() {
				return true;
			},
			hasWinner : function() {
				return null;
			}
		};
	}

	function toState(result) {
		return {
			state : function() {
				return this.winner() == null ? 'IN_PROGRESS' : 'WINNER';
			},
			winner : function() {
				return result.winner;
			},
			cells : function() {
				return result.cells;
			},
			isInProgress : function() {
				return this.state() === 'IN_PROGRESS';
			},
			isInStale : function() {
				return false;
			},
			hasWinner : function() {
				return this.winner() != null;
			}
		};
	}

	if (this.board.countMoves == this.board.getWidth() * this.board.getHeight()) {
		return staleResponse();
	}

	return toState(this.getWinner());
};

BoardStateEvaluator.prototype.getWinner = function() {
	var result = this.evaluateRows();

	if (result == null) {
		result = this.evaluateCols();
	}

	if (result == null) {
		result = this.evaluateLRDiagonals();
	}

	if (result == null) {
		result = this.evaluateRLDiagonals();
	}

	if (result == null) {
		result = {
			winner : null,
			cells : []
		};
	}

	return result;
};

BoardStateEvaluator.prototype.evaluateRows = function() {
	var result = null;

	for (var i = 0; i < this.board.getHeight(); i++) {
		result = this.evaluateIterator(new BoardIterator(this.board, i, 0, 0, 1));
		if (result != null) {
			break;
		}
	}

	return result;
};

BoardStateEvaluator.prototype.evaluateCols = function() {
	var result = null;

	for (var i = 0; i < this.board.getWidth(); i++) {
		result = this.evaluateIterator(new BoardIterator(this.board, 0, i, 1, 0));
		if (result != null) {
			break;
		}
	}

	return result;
};

BoardStateEvaluator.prototype.evaluateLRDiagonals = function() {
	var result = null;

	for (var col = 0; col < this.board.getWidth(); col++) {
		result = this.evaluateIterator(new BoardIterator(this.board, 0, col, 1, 1));
		if (result != null) {
			break;
		}
	}

	if (result == null) {
		for (var row = 1; row < this.board.getHeight(); row++) {
			result = this.evaluateIterator(new BoardIterator(this.board, row, 0, 1, 1));
			if (result != null) {
				break;
			}
		}
	}

	return result;
};

BoardStateEvaluator.prototype.evaluateRLDiagonals = function() {
	var result = null;

	for (var col = 0; col < this.board.getWidth(); col++) {
		result = this.evaluateIterator(new BoardIterator(this.board, 0, col, 1, -1));
		if (result != null) {
			break;
		}
	}

	if (result == null) {
		var lastColIndex = this.board.getWidth() - 1;
		for (var row = 1; row < this.board.getHeight(); row++) {
			result = this.evaluateIterator(new BoardIterator(this.board, row, lastColIndex, 1, -1));
			if (result != null) {
				break;
			}
		}
	}

	return result;
};

BoardStateEvaluator.prototype.evaluateIterator = function(iterator) {
	var result = null;
	var count = 1;
	var winningcells = [];
	var lastValue = null;

	while (iterator.hasNext()) {
		var cellVal = iterator.next();
		if (cellVal != Player.NONE && cellVal == lastValue) {
			count++;
			winningcells.push({
				row : iterator.currentRow(),
				col : iterator.currentCol()
			});

			if (count >= this.board.NUM_TO_WIN) {
				result = {
					winner : cellVal,
					cells : winningcells
				};
				break;
			}
		} else {
			count = 1;
			winningcells = [];
			winningcells.push({
				row : iterator.currentRow(),
				col : iterator.currentCol()
			});
		}

		lastValue = cellVal;
	}

	return result;
};

/**
 * BoardIterator
 * 
 * @param {GameBoard} board The gameBoard where to iterate
 * @param {number} row The starting row index
 * @param {number} col The starting column index
 * @param {number} rowIncrement The increment on each step in the row index
 * @param {number} colIncrement The increment on each step in the column index
 */
function BoardIterator(board, row, col, rowIncrement, colIncrement) {
	this.board = board;
	this.rowIncrement = rowIncrement;
	this.colIncrement = colIncrement;
	this.row = row - rowIncrement;
	this.col = col - colIncrement;
}

BoardIterator.prototype.hasNext = function() {
	return this.colInBound(this.col + this.colIncrement) && this.rowInBound(this.row + this.rowIncrement);
};

BoardIterator.prototype.next = function() {
	if (this.hasNext()) {
		this.row += this.rowIncrement;
		this.col += this.colIncrement;

		return this.board.get(this.row, this.col);
	} else {
		throw 'Iterator end already reached.';
	}
};

BoardIterator.prototype.currentRow = function() {
	return this.row;
};

BoardIterator.prototype.currentCol = function() {
	return this.col;
};

BoardIterator.prototype.colInBound = function(col) {
	return (col) < this.board.getWidth();
};

BoardIterator.prototype.rowInBound = function(row) {
	return (row) < this.board.getHeight();
};

/**
 * Player enumeration
 */
function PlayerEnum(textValue) {
	this.textValue = textValue;
}

PlayerEnum.prototype.X = new PlayerEnum('X');
PlayerEnum.prototype.O = new PlayerEnum('O');
PlayerEnum.prototype.NONE = new PlayerEnum('');

PlayerEnum.prototype.next = function() {
	return (this === PlayerEnum.prototype.X) ? PlayerEnum.prototype.O : PlayerEnum.prototype.X;
};

PlayerEnum.prototype.toString = function() {
	return this.textValue;
};

var Player = {
	X : PlayerEnum.prototype.X,
	O : PlayerEnum.prototype.O,
	NONE : PlayerEnum.prototype.NONE
};